﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Volo.Abp.Caching;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Features;
using Volo.Abp.Uow;
using Xms.Abp.Domain;
using Xms.Abp.Domain.Repositories;

namespace FeatureManagement;

[Dependency(ReplaceServices = true)]
[ExposeServices(typeof(IFeatureManagementStore), typeof(IFeatureStore))]
public class FeatureManagementStore : XmsDomainService, IFeatureManagementStore, IFeatureStore, ITransientDependency
{
    protected IDistributedCache<FeatureValueCacheItem> Cache => LazyInject<IDistributedCache<FeatureValueCacheItem>>();
    protected IFeatureDefinitionManager FeatureDefinitionManager => LazyInject<IFeatureDefinitionManager>();
    protected IXmsRepository<FeatureValue, Guid> FeatureValueRepository => LazyInject<IXmsRepository<FeatureValue, Guid>>();

    [UnitOfWork]
    public virtual async Task<string> GetOrNullAsync(string name, string providerName, string providerKey)
    {
        var cacheItem = await GetCacheItemAsync(name, providerName, providerKey);
        return cacheItem.Value;
    }

    [UnitOfWork]
    public virtual async Task<List<FeatureNameValue>> GetOrNullAsync(string providerName, string providerKey, List<string> names)
    {
        var cacheItem = await GetCacheItemAsync(providerName, providerKey, names);
        return cacheItem.Select(r => new FeatureNameValue(r.Key, r.Value.Value)).ToList();
    }

    [UnitOfWork]
    public virtual async Task SetAsync(string name, string value, string providerName, string providerKey)
    {
        var featureValue = await FeatureValueRepository.FindAsync(s => s.Name == name && s.ProviderName == providerName && s.ProviderKey == providerKey);
        if (featureValue == null)
        {
            featureValue = new FeatureValue(GuidGenerator.Create(), name, value, providerName, providerKey);
            await FeatureValueRepository.InsertAsync(featureValue);
        }
        else
        {
            featureValue.Value = value;
            await FeatureValueRepository.UpdateAsync(featureValue);
        }

        await Cache.SetAsync(CalculateCacheKey(name, providerName, providerKey), new FeatureValueCacheItem(featureValue?.Value), considerUow: true);
    }

    [UnitOfWork]
    public virtual async Task SetAsync(string providerName, string providerKey, List<FeatureNameValue> nameValues)
    {
        var featureDefinitions = FeatureDefinitionManager.GetAll();
        var featureValuesDict = (await FeatureValueRepository.GetListAsync(s => s.ProviderName == providerName && s.ProviderKey == providerKey)).ToDictionary(r => r.Name);
        var nameValuesDict = nameValues.ToDictionary(r => r.Name);
        var adds = nameValuesDict.Keys.Except(featureValuesDict.Keys).Select(r => new FeatureValue(GuidGenerator.Create(), r, nameValuesDict[r].Value, providerName, providerKey));
        await FeatureValueRepository.InsertManyAsync(adds);
        var removes = featureValuesDict.Keys.Except(nameValuesDict.Keys);
        var updates = nameValuesDict.Keys.Intersect(nameValuesDict.Keys).Where(r => featureValuesDict[r].Value != nameValuesDict[r].Value).Select(r => featureValuesDict[r]);
        foreach (var update in updates)
        {
            update.Value = nameValuesDict[update.Name].Value;
        }
        var removeValues = removes.Select(r => featureValuesDict[r]);
        foreach (var removeValue in removeValues)
        {
            removeValue.Value = null;
        }
        var updateValues = updates.Union(removeValues);
        await FeatureValueRepository.UpdateManyAsync(updateValues);

        await Cache.SetManyAsync(updateValues.ToDictionary(r => CalculateCacheKey(r.Name, providerName, providerKey), r => new FeatureValueCacheItem(r.Value)), considerUow: true);
    }

    [UnitOfWork]
    public virtual async Task DeleteAsync(string name, string providerName, string providerKey)
    {
        await FeatureValueRepository.DeleteAsync(s => s.Name == name && s.ProviderName == providerName && s.ProviderKey == providerKey);
        await Cache.RemoveAsync(CalculateCacheKey(name, providerName, providerKey), considerUow: true);
    }

    [UnitOfWork]
    public virtual async Task DeleteAsync(string providerName, string providerKey, List<string> names)
    {
        await FeatureValueRepository.DeleteAsync(s => s.ProviderName == providerName && s.ProviderKey == providerKey && names.Contains(s.Name));
        await Cache.RemoveManyAsync(names.Select(r => CalculateCacheKey(r, providerName, providerKey)), considerUow: true);
    }

    [UnitOfWork]
    public virtual async Task DeleteAllAsync(string providerName, string providerKey)
    {
        await FeatureValueRepository.DeleteAsync(s => s.ProviderName == providerName && s.ProviderKey == providerKey);
        var featureDefinitions = FeatureDefinitionManager.GetAll();
        var names = featureDefinitions.Select(r => r.Name);
        await Cache.RemoveManyAsync(names.Select(r => CalculateCacheKey(r, providerName, providerKey)), considerUow: true);
    }

    protected virtual async Task<FeatureValueCacheItem> GetCacheItemAsync(string name, string providerName, string providerKey)
    {
        var cacheKey = CalculateCacheKey(name, providerName, providerKey);
        var cacheItem = await Cache.GetAsync(cacheKey, considerUow: true);

        if (cacheItem != null)
        {
            return cacheItem;
        }

        cacheItem = new FeatureValueCacheItem(null);

        await SetCacheItemsAsync(providerName, providerKey, name, cacheItem);

        return cacheItem;
    }

    private async Task SetCacheItemsAsync(
        string providerName,
        string providerKey,
        string currentName,
        FeatureValueCacheItem currentCacheItem)
    {
        var featureDefinitions = FeatureDefinitionManager.GetAll();
        var featuresDictionary = (await (await FeatureValueRepository.AsNoTrackingAsync()).Where(s => s.ProviderName == providerName && s.ProviderKey == providerKey).ToListAsync())
            .ToDictionary(s => s.Name, s => s.Value);

        var cacheItems = new Dictionary<string, FeatureValueCacheItem>();

        foreach (var featureDefinition in featureDefinitions)
        {
            var featureValue = featuresDictionary.GetOrDefault(featureDefinition.Name);

            var key = CalculateCacheKey(featureDefinition.Name, providerName, providerKey);
            cacheItems[key] = new FeatureValueCacheItem(featureValue);

            if (featureDefinition.Name == currentName)
            {
                currentCacheItem.Value = featureValue;
            }
        }

        await Cache.SetManyAsync(cacheItems, considerUow: true);
    }

    protected virtual async Task<Dictionary<string, FeatureValueCacheItem>> GetCacheItemAsync(string providerName, string providerKey, List<string> names)
    {
        var cacheKeys = names.ToDictionary(r => r, r => CalculateCacheKey(r, providerName, providerKey));
        var cacheItems = (await Cache.GetManyAsync(cacheKeys.Values, considerUow: true)).ToDictionary(r => r.Key, r => r.Value);

        if (cacheItems != null && cacheItems.Count == names.Count)
        {
            return cacheItems;
        }
        if(cacheItems == null)
        {
            cacheItems = names.ToDictionary(r => r, r => new FeatureValueCacheItem(null));
        }
        else
        {
            foreach (var name in names)
            {
                if (!cacheItems.ContainsKey(name))
                {
                    cacheItems[name] = new FeatureValueCacheItem(null);
                }
            }
        }

        await SetCacheItemsAsync(providerName, providerKey, cacheItems);

        return cacheItems;
    }

    private async Task SetCacheItemsAsync(
        string providerName,
        string providerKey,
        Dictionary<string, FeatureValueCacheItem> currentCacheItems)
    {
        var featureDefinitions = FeatureDefinitionManager.GetAll();
        var featuresDictionary = (await (await FeatureValueRepository.AsNoTrackingAsync()).Where(s => s.ProviderName == providerName && s.ProviderKey == providerKey).ToListAsync())
            .ToDictionary(s => s.Name, s => s.Value);

        var cacheItems = new Dictionary<string, FeatureValueCacheItem>();

        foreach (var featureDefinition in featureDefinitions)
        {
            var featureValue = featuresDictionary.GetOrDefault(featureDefinition.Name);
            var key = CalculateCacheKey(featureDefinition.Name, providerName, providerKey);
            cacheItems[key] = new FeatureValueCacheItem(featureValue);

            if (currentCacheItems.ContainsKey(featureDefinition.Name))
            {
                currentCacheItems[featureDefinition.Name].Value = featureValue;
            }
        }

        await Cache.SetManyAsync(cacheItems, considerUow: true);
    }

    protected virtual string CalculateCacheKey(string name, string providerName, string providerKey)
    {
        return FeatureValueCacheItem.CalculateCacheKey(name, providerName, providerKey);
    }

    [UnitOfWork]
    public async Task<List<FeatureNameValue>> GetAllAsync(string providerName, string providerKey)
    {
        var featureDefinitions = FeatureDefinitionManager.GetAll();
        var names = featureDefinitions.Select(r => r.Name).ToList();
        return await GetOrNullAsync(providerName, providerKey, names);
    }
}
